package logger

import (
	"errors"
	"fmt"
	"gitee.com/sunny-zhangqiang/zcache_new/comm"
	"gitee.com/sunny-zhangqiang/zcache_new/comm/executor"
	"os"
	"path/filepath"
	"sync"
	"time"
)

const (
	minFlushIntervalSec = 1
	minFlushBufferSize  = 4 << 20
)

var (
	nothingErr = errors.New("do nothing")
)

type outBufPool struct {
	c chan []byte
}

func (pool *outBufPool) get(block bool) []byte {
	if block {
		return <-pool.c
	}

	select {
	case b := <-pool.c:
		return b
	default:
		return nil
	}
}

func (pool *outBufPool) put(b []byte) {
	pool.c <- b
}

func newOutBufPool(size int64, n int) *outBufPool {
	pool := &outBufPool{
		c: make(chan []byte, n),
	}

	for i := 0; i < n; i++ {
		pool.put(make([]byte, size))
	}

	return pool
}

type scrollingResult struct {
	file         *os.File
	realFileName string
	err          error
}

type FileOutputAsync struct {
	sync.Mutex
	logFileName      string
	realFileName     string
	scrollingMode    ScrollingMode
	blockSend        bool
	flushIntervalSec int64
	flushBufferSize  int64
	file             *os.File
	curBuffer        []byte
	flushBufferCh    chan []byte
	bufPool          *outBufPool
	lastSwitchTime   int64
	flushTask        *executor.TimerTask
	scrollingCh      chan scrollingResult
	closed           bool
	closeCh          chan struct{}
}

func (fo *FileOutputAsync) Output(_ Level, s string) {
	fo.Lock()
	defer fo.Unlock()

	if fo.closed {
		return
	}

	if fo.curBuffer == nil {
		fo.curBuffer = fo.bufPool.get(fo.blockSend)[:0]
		if fo.curBuffer == nil {
			fmt.Println("loss")
			return
		}
	}

	slen := int64(len(s))
	if slen > fo.flushBufferSize {
		slen = fo.flushBufferSize
	}

	used := int64(len(fo.curBuffer))

	if slen+used > fo.flushBufferSize {
		fo.flushBufferCh <- fo.curBuffer
		fo.lastSwitchTime = time.Now().Unix()
		fo.curBuffer = nil
		fo.curBuffer = fo.bufPool.get(fo.blockSend)[:0]
		if fo.curBuffer == nil {
			fmt.Println("loss")
			return
		}
		used = 0
	}

	ss := comm.String2Bytes(s)
	copy(fo.curBuffer[used:used+slen], ss)
	fo.curBuffer = fo.curBuffer[:used+slen]

	if used+slen >= fo.flushBufferSize {
		fo.flushBufferCh <- fo.curBuffer
		fo.lastSwitchTime = time.Now().Unix()
		fo.curBuffer = nil
	}
}

func (fo *FileOutputAsync) Flush() {
	fo.Lock()
	defer fo.Unlock()

	if fo.closed {
		return
	}

	if fo.curBuffer == nil || len(fo.curBuffer) == 0 {
		return
	}

	fo.flushBufferCh <- fo.curBuffer
	fo.curBuffer = nil
	fo.lastSwitchTime = time.Now().Unix()
}

func (fo *FileOutputAsync) Close() {
	fo.Lock()
	defer fo.Unlock()

	fo.closed = true
	executor.TaskTimerExecutor.DelTask(fo.flushTask)
	fo.closeCh <- struct{}{}
}

func TimerFlush(ctx interface{}) {
	fo := ctx.(*FileOutputAsync)

	fo.Lock()
	defer fo.Unlock()

	if fo.closed {
		return
	}

	if time.Now().Unix() < fo.lastSwitchTime+fo.flushIntervalSec {
		return
	}

	if fo.curBuffer == nil || len(fo.curBuffer) == 0 {
		return
	}

	fo.flushBufferCh <- fo.curBuffer
	fo.curBuffer = nil
	fo.lastSwitchTime = time.Now().Unix()
}

func (fo *FileOutputAsync) tryScrollingLogFile() {
	realFileName, err := timeFileName(fo.logFileName, fo.scrollingMode)
	if err != nil {
		fo.scrollingCh <- scrollingResult{err: err}
		return
	}

	if realFileName == fo.realFileName {
		fo.scrollingCh <- scrollingResult{err: nothingErr}
		return
	}

	file, err := os.OpenFile(realFileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		fo.scrollingCh <- scrollingResult{err: err}
		return
	}

	os.Remove(fo.logFileName)
	os.Symlink(realFileName, fo.logFileName)

	fo.scrollingCh <- scrollingResult{file: file, realFileName: realFileName}
}

func (fo *FileOutputAsync) flushLoop() {
	closed := false
	scrolling := false
	scrollingTicker := time.NewTicker(time.Second * 60)
	defer scrollingTicker.Stop()

	for {
		if closed && len(fo.flushBufferCh) == 0 {
			break
		}

		select {
		case <-fo.closeCh:
			closed = true

		case <-scrollingTicker.C:
			if scrolling {
				continue
			}
			scrolling = true
			go fo.tryScrollingLogFile()

		case res := <-fo.scrollingCh:
			scrolling = false
			if res.err != nil {
				continue
			}
			if fo.file != nil {
				fo.file.Close()
			}
			fo.file = res.file
			fo.realFileName = res.realFileName

		case b := <-fo.flushBufferCh:
			fo.file.Write(b)
			fo.bufPool.put(b)
		}
	}
}

// NewFileOutputAsync
// 在输出能力足够的情况下, 使用blockSend能取得最好的性能, 日志输出能力高达200W/s
// 在输出能力不足的情况下, 使用非blockSend会通过丢日志的方式避免 Output接口长时间卡顿, 导致业务卡顿. 但是它的日志输出能力只有80W/s
func NewFileOutputAsync(logFileName string, model ScrollingMode, blockSend bool,
	flushIntervalSec int64, flushBufferSize int64, nFlushBuffer int) (*FileOutputAsync, error) {

	if flushIntervalSec < minFlushIntervalSec {
		flushIntervalSec = minFlushIntervalSec
	}

	if flushBufferSize < minFlushBufferSize {
		flushBufferSize = minFlushBufferSize
	}

	if nFlushBuffer <= 2 {
		nFlushBuffer = 2
	}

	logFileName, err := filepath.Abs(logFileName)
	if err != nil {
		return nil, err
	}

	if logFileName == "/" {
		return nil, errors.New("not allowed logFileName")
	}

	realFileName, err := timeFileName(logFileName, model)
	if err != nil {
		return nil, err
	}

	_ = os.MkdirAll(filepath.Dir(realFileName), 0755)
	file, err := os.OpenFile(realFileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return nil, err
	}

	fo := &FileOutputAsync{
		logFileName:      logFileName,
		realFileName:     realFileName,
		scrollingMode:    model,
		blockSend:        blockSend,
		flushIntervalSec: flushIntervalSec,
		flushBufferSize:  flushBufferSize,
		file:             file,
		flushBufferCh:    make(chan []byte, nFlushBuffer),
		bufPool:          newOutBufPool(flushBufferSize, nFlushBuffer),
		scrollingCh:      make(chan scrollingResult, 1),
		closeCh:          make(chan struct{}, 1),
	}

	fo.curBuffer = fo.bufPool.get(true)[:0]

	if model != ScrollingNone {
		os.Remove(logFileName)
		os.Symlink(realFileName, logFileName)
	}

	flushTask := &executor.TimerTask{
		Ctx:      fo,
		Handler:  TimerFlush,
		Interval: int64(flushIntervalSec),
	}
	fo.flushTask = flushTask

	go fo.flushLoop()
	executor.TaskTimerExecutor.AddTask(flushTask)

	return fo, nil
}
